/**
 * Copyright (C) 2007 The AsyncMail Group <asyncmail@googlegroups.com>
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *         http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.googlecode.asyncmail.smtpclient;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.logging.Log;
import org.apache.mina.common.IdleStatus;
import org.apache.mina.common.IoHandlerAdapter;
import org.apache.mina.common.IoSession;
import org.apache.mina.common.TrafficMask;
import org.apache.mina.filter.StreamWriteFilter;
import org.springframework.beans.factory.annotation.Required;

import com.googlecode.asyncmail.common.MailAddress;
import com.googlecode.asyncmail.common.MailEnvelope;
import com.googlecode.asyncmail.queue.MailQueueItem;
import com.googlecode.asyncmail.smtp.SMTPRequest;
import com.googlecode.asyncmail.smtp.SMTPRequestImpl;
import com.googlecode.asyncmail.smtp.SMTPResponse;
import com.googlecode.asyncmail.smtp.SMTPResponseImpl;

import java.io.InputStream;
import java.util.LinkedList;
import java.util.Queue;

/*
 * TODO: Handle bounces
 */

public class SMTPDeliveryIoHandler extends IoHandlerAdapter {

    private static final String PIPELINING_SUPPORTED = "PIPELINING_SUPPORTED";

    private static final String SENT_COMMANDS = "SENT_COMMANDS";

    private static final String COMMANDS_QUEUE = "COMMANDS_QUEUE";

    private static final String STARTTLS_SUPPORTED = "STARTTLS_SUPPORTED";

    public static final String ERROR = "ERROR";

    public static final String ERROR_MESSAGE = "ERROR_MESSAGE";

    public static final String ERROR_CODE = "ERROR_CODE";

    private Log logger;

    @Required
    public void setLogger(Log logger) {
        this.logger = logger;
    }

    @Override
    public void exceptionCaught(IoSession arg0, Throwable arg1)
            throws Exception {
        System.err.println("exceptionCaught: " + arg1.getMessage());
        System.err.println("error on local conn: " + arg0.getLocalAddress());
        arg1.printStackTrace();
        // super.exceptionCaught(arg0, arg1);
    }

    @SuppressWarnings("unchecked")
    @Override
    public void messageReceived(IoSession arg0, Object arg1) throws Exception {
        logger.debug("messageReceived " + arg1);
        if (arg1 instanceof SMTPResponseImpl) {
            Queue<SMTPRequestImpl> commandQueue = (Queue<SMTPRequestImpl>) arg0
                    .getAttribute(SENT_COMMANDS);
            if (commandQueue == null) {
                commandQueue = new LinkedList<SMTPRequestImpl>();
                arg0.setAttribute(SENT_COMMANDS, commandQueue);
            }
            handleCommandResponse(arg0, commandQueue.poll(),
                    (SMTPResponse) arg1);
        } else {
            logger.error("WEIRD REPLY: " + arg1.getClass() + " => " + arg1);
            super.messageReceived(arg0, arg1);
        }
    }

    private void handleCommandResponse(IoSession arg0, SMTPRequest request,
            SMTPResponse arg1) {
        // welcome message
        MailQueueItem qItem = (MailQueueItem) arg0
                .getAttribute(MailQueueItem.MAIL_QUEUE_ITEM);
        MailEnvelope mail = qItem.getMailEnvelope();

        if ((request == null && "220".equals(arg1.getRetCode()))
                || ("STARTTLS".equals(request.getCommand()) && "220"
                        .equals(arg1.getRetCode()))) {
            /*
             * TODO: Fix SSL if (request != null &&
             * "STARTTLS".equals(request.getCommand())) { //
             * System.err.println("starting TLS....");
             * arg0.setTrafficMask(TrafficMask.WRITE); SSLFilter filter = new
             * SSLFilter(sslContext); filter.setUseClientMode(true);
             * arg0.getFilterChain().addFirst("sslFilter", filter);
             * arg0.setTrafficMask(TrafficMask.ALL);
             * arg0.setAttribute("STARTTLS_ACTIVATED"); }
             * 
             */

            // TO: make HELO/EHLO configureable
            SMTPRequestImpl newRequest = new SMTPRequestImpl("EHLO", "me.void.it.");
            // should never enqueue, but always write!
            enqueueRequest(arg0, newRequest);
        } else if (("EHLO".equals(request.getCommand()) && "250".equals(arg1
                .getRetCode()))
                || "235".equals(arg1.getRetCode())) {

            if ("235".equals(arg1.getRetCode())) {
                arg0.setAttribute("AUTHENTICATED");
            } else {
                // use AUTH
                if (arg1.getLines().contains("PIPELINING")) {
                    arg0.setAttribute(PIPELINING_SUPPORTED);
                }
                // use STARTTLS
                if (arg1.getLines().contains("STARTTLS")) {
                    arg0.setAttribute(STARTTLS_SUPPORTED);
                }
            }

            if (arg0.containsAttribute(STARTTLS_SUPPORTED)
                    && !arg0.containsAttribute("STARTTLS_ACTIVATED")) {
                SMTPRequestImpl request2 = new SMTPRequestImpl("STARTTLS", null);
                enqueueRequest(arg0, request2);
            } else if (arg1.getLines().contains("AUTH LOGIN PLAIN")
                    && !arg0.containsAttribute("AUTHENTICATED")) {
                // try the AUTH login
                SMTPRequestImpl request2 = new SMTPRequestImpl("AUTH", "PLAIN");
                // +new String(Base64.encodeBase64("marissa".getBytes()))
                enqueueRequest(arg0, request2);
            } else {
                SMTPRequestImpl request2;
                MailAddress from = mail.getSender();
                if (from == null) {
                    request2 = new SMTPRequestImpl("MAIL", "FROM:<>");
                } else {
                    request2 = new SMTPRequestImpl("MAIL", "FROM:<"
                            + mail.getSender().getAddressAsString() + ">");
                }

                enqueueRequest(arg0, request2);

                SMTPRequestImpl request3 = new SMTPRequestImpl("RCPT",
                        "TO:<"
                                + ((MailAddress) arg0
                                        .getAttribute("CURRENT_RECIPIENT"))
                                        .getAddressAsString() + ">");
                enqueueRequest(arg0, request3);

                SMTPRequestImpl request4 = new SMTPRequestImpl("DATA", null);
                enqueueRequest(arg0, request4);
            }
        } else if ("AUTH".equals(request.getCommand())) {
            // System.err.println(arg1);
            if ("334".equals(arg1.getRetCode())) {
                SMTPRequestImpl newRequest = new SMTPRequestImpl(new String(Base64
                        .encodeBase64("\0marissa\0koala".getBytes())), null);
                enqueueRequest(arg0, newRequest);
            }
        } else if ("RCPT".equals(request.getCommand())
                || "MAIL".equals(request.getCommand())) {
            if (arg1.getRetCode().startsWith("4")) {
                if (arg0.containsAttribute(ERROR) == false) {
                    // TEMPORY PROBLEM REQUEUE
                    arg0.setAttribute(ERROR_MESSAGE, arg1.getLines());
                    arg0.setAttribute(ERROR_CODE, arg1.getRetCode());
                    arg0.setAttribute(ERROR, "TEMP");

                    // No need to process on error
                    SMTPRequestImpl request2 = new SMTPRequestImpl("QUIT", null);
                    enqueueRequest(arg0, request2);
                }
            } else if (arg1.getRetCode().startsWith("5")) {
                // PERMANENT PROBLEM BOUNCE!!!
                if (arg0.containsAttribute(ERROR) == false) {
                    arg0.setAttribute(ERROR_MESSAGE, arg1.getLines());
                    arg0.setAttribute(ERROR_CODE, arg1.getRetCode());
                    arg0.setAttribute(ERROR, "PERM");

                    // No need to process on error
                    SMTPRequestImpl request2 = new SMTPRequestImpl("QUIT", null);
                    enqueueRequest(arg0, request2);
                }
            }
            // empty the queue
            checkQueue(arg0);
        } else if ("DATA".equals(request.getCommand())) {
            if ("354".equals(arg1.getRetCode())) {
                arg0.setTrafficMask(TrafficMask.WRITE);
                arg0.getFilterChain().addLast("streamFilter",
                        new StreamWriteFilter());
                arg0.write(mail.getBodyInputStream());
            }
        } else if (".".equals(request.getCommand())) {

            if (arg0.containsAttribute("ERROR") == false) {
                // Remove current recipient of the mail
                mail.getRecipients().remove(
                        arg0.getAttribute("CURRENT_RECIPIENT"));
            }

            SMTPRequestImpl request2 = new SMTPRequestImpl("QUIT", null);
            enqueueRequest(arg0, request2);
        } else if ("QUIT".equals(request.getCommand())) {
            if ("221".equals(arg1.getRetCode())) {
                arg0.close();

            } else {
                logger.error("Bad result on close!");
            }
        } else {
            logger.error("WHAT? " + request + " | " + arg1);
        }
    }

    /**
     * @param arg0
     * @param request3
     */
    @SuppressWarnings("unchecked")
    private void enqueueRequest(IoSession arg0, SMTPRequestImpl request3) {
        Queue<SMTPRequestImpl> linkedList = ((Queue<SMTPRequestImpl>) arg0
                .getAttribute(SENT_COMMANDS));
        if (linkedList.size() > 0
                && !arg0.containsAttribute(PIPELINING_SUPPORTED)) {
            logger.debug("#### PIPELINING NOT SUPPORTED, ENQUEUE NEW COMMAND");
            Queue<SMTPRequestImpl> queue = (Queue<SMTPRequestImpl>) arg0
                    .getAttribute(COMMANDS_QUEUE);
            if (queue == null) {
                queue = new LinkedList<SMTPRequestImpl>();
                arg0.setAttribute(COMMANDS_QUEUE, queue);
            }
            logger.debug("COMMAND ENQUEUED");
            queue.add(request3);
        } else {
            linkedList.add(request3);
            arg0.write(request3);
        }
    }

    /**
     * @param arg0
     */
    @SuppressWarnings("unchecked")
    private void checkQueue(IoSession arg0) {
        Queue<SMTPRequestImpl> queue = (Queue<SMTPRequestImpl>) arg0
                .getAttribute(COMMANDS_QUEUE);
        if (queue != null) {
            logger.debug("#### PIPELINING NOT SUPPORTED, SEND QUEUED COMMAND");
            SMTPRequestImpl req = queue.poll();
            enqueueRequest(arg0, req);
        }
    }

    @Override
    public void messageSent(IoSession arg0, Object arg1) throws Exception {
        if (arg1 instanceof InputStream) {
            ((InputStream) arg1).close();
            arg0.getFilterChain().remove("streamFilter");
            arg0.setTrafficMask(TrafficMask.ALL);
            SMTPRequestImpl request = new SMTPRequestImpl(".", null);
            enqueueRequest(arg0, request);
            // request = new SMTPRequestImpl("QUIT",null);
            // enqueueRequest(arg0, request);
            // ((Queue<SMTPRequestImpl>)
            // arg0.getAttribute(SENT_COMMANDS)).add(request);
            // arg0.write(".");
        } else {
            // System.out.println("messageSent");
            super.messageSent(arg0, arg1);
        }
    }

    @Override
    public void sessionClosed(IoSession arg0) throws Exception {
        logger.debug("Session was closed");
        // System.out.println("messageClosed");
        super.sessionClosed(arg0);
    }

    @Override
    public void sessionCreated(IoSession arg0) throws Exception {
        logger.debug("session created");
        // System.err.println("sessionCreated: "+System.identityHashCode(arg0));
        // disable traffic while we receive the notification of connection
        // established
        arg0.setTrafficMask(TrafficMask.NONE);
        super.sessionCreated(arg0);
    }

    @Override
    public void sessionIdle(IoSession arg0, IdleStatus arg1) throws Exception {
        logger.debug("sessionIdle");
        super.sessionIdle(arg0, arg1);
    }

    @Override
    public void sessionOpened(IoSession arg0) throws Exception {
        // System.err.println("sessionOpened: "+System.identityHashCode(arg0));
        super.sessionOpened(arg0);
    }

}
